feat(iam): add an AWS IAM adapter with a schema-declared kind facet - #162
feat(iam): add an AWS IAM adapter with a schema-declared kind facet#162TheSaifZaman wants to merge 6 commits into
Conversation
|
| Filename | Overview |
|---|---|
| packages/api/src/adapter-aws/AwsIamAdapter.ts | IAM adapter with kind-prefixed ids; policy inspect loads default version via GetPolicyVersion and surfaces decoded document (or unavailable flag). |
| packages/api/src/adapter-aws/AwsIamAdapter.test.ts | Covers kind facet, pagination, create validation, and policy-document inspect including encoded/literal docs and version-lookup failure. |
| packages/api/src/routes/clouds.ts | Populates ResourceQuery.filters only for schema-declared facet names. |
| packages/api/src/cloud-spi/types.ts | Adds filters on ResourceQuery and iam resource types. |
Reviews (5): Last reviewed commit: "docs(iam): state that the kind facet is ..." | Re-trigger Greptile
There was a problem hiding this comment.
Thank you for a genuinely careful adapter. The details show real attention to the IAM contract: paging every List call on IsTruncated and Marker, listing policies with Scope Local so the account's own policies are not buried (with a test holding that line), decoding the percent-encoded trust policy, fetching the policy document only on inspect to avoid an N+1, and keying policies by ARN because GetPolicy and DeletePolicy take one. The hermetic test suite and the schema-gated facet plumbing in the route are both well thought through, and both lockfiles are regenerated, which is the most common CI trap here.
One coordination note before the substance: PR #145 has been adding AWS IAM to the Cloud Explorer since July 18 and is actively iterating through review, and the two PRs touch the same files (adapter, schema, catalog row, and the shared type unions). That is nobody's fault, but the direction (one identity category starting with users, or this three-kind service with a facet) is mine to decide, and I am working through it now. It would be worth holding off on rework here until that lands, so neither of you burns time rebasing twice.
On the merits, two questions:
-
The kind facet currently has no way to be used from the UI. DynamicResourceView only sends
search(cloudProxyClient passes just that one param), andschema.filtersis not rendered as controls anywhere in the frontend, so today the facet is reachable only by calling the API directly. Given your own (good) argument that schema surface needs a consumer landing with it, could you add the small piece that renders non-search filters from the schema (a select next to the search box in DynamicResourceView would do), or say explicitly in the PR that the facet is API-only for now and the UI control is a follow-up? Either is fine, it just should be a stated choice rather than a gap. -
Nit, not a blocker: the README table row looks hand-added. AGENTS.md asks for the table to be regenerated with
bun run scripts/service-matrix.tsso it cannot drift; could you confirm the row matches the script's output?
Everything else (catalog registration, typed errors, capability guard, tests colocated, no fake data, real empty states) follows the current pattern exactly. Nice work.
Both lockfiles updated with the manifest: CI installs the workspace with pnpm install --frozen-lockfile and the api package with bun install --frozen-lockfile, so updating only one fails a job.
Adds `ResourceQuery.filters`, populated by the list route from query params — but only for the facet names the service's own schema declares in `filters`. An undeclared or stale param is ignored rather than reaching an adapter that never asked for it, the same way the service catalog gates unknown service slugs. Free-text search cannot express "only roles", which is what a category holding several kinds of resource needs. `search` stays a separate field on ResourceQuery, so every existing adapter is untouched.
Adds an `iam` service under Security covering users, roles and
customer-managed policies, and is the first consumer of the schema-declared
query facets added in the previous commit.
The facet is the point: IAM holds three kinds of resource in one category,
and `kind=roles` is not something a search box can express. Unset, list
returns all three together, which is what an audit view wants.
Decisions that follow from the API's shape:
- Resource ids are `kind/identifier` — user/alice, role/deployer, and
policy/{arn} for policies, because GetPolicy and DeletePolicy take an
ARN rather than a name. A bare name would be ambiguous across kinds and
would not tell get or delete which API to call. The ids survive the
generic route because HttpClient encodes path params.
- Policies are listed with Scope=Local. Real IAM otherwise returns close
to a thousand AWS-managed policies and buries the account's own; the
emulator returns few enough that only a test can hold this line.
- A role's trust policy is decoded from the percent-encoded form IAM
returns, which is unreadable raw in the inspector.
- create validates per kind: a role needs a trust policy and a policy
needs a document, both of which must parse as JSON. The message names
the field and the kind, because the form shows both fields for every
kind and a flat form cannot mark them conditionally required.
- Every List call pages on IsTruncated/Marker.
Verified end to end through the route: the nav entry, create for all three
kinds, list with and without the facet, an undeclared query param being
ignored, an unknown facet value returning 400, inspect by encoded id
including a policy ARN, the decoded trust policy, delete for each kind,
and 404 for a missing entity.
A role's inspector surfaced its decoded trust policy but a policy showed only metadata, so the document being audited — the reason to open a policy at all — was never visible. get() now loads the default version with GetPolicyVersion and exposes the decoded document. Only on inspect: doing it during list would issue one extra call per row, which a test pins. The decode handles both shapes. Real IAM percent-encodes the document; the local runtime returns it literal, and decodeURIComponent leaves a string with no escapes unchanged, so neither is corrupted. A failed version lookup degrades the row with metadata.policyDocumentUnavailable rather than failing the inspect, since the rest of the policy metadata is still worth showing.
…nion
The frontend keeps its own copy of CloudResource.type — the packages are
independent and share no dependency — so widening the API union without
widening this one leaves the frontend contract behind the payloads the
API actually returns.
KnownResourceType | (string & {}). This branch is off main, where the
union is still closed, so the member is added directly; if floci-io#156 merges
first the one-line conflict resolves by moving it into KnownResourceType.
bd876f9 to
5911b5a
Compare
|
Rebased onto Gate green after the rebase: lint, type-check, 459 tests, build. |
Adds a `loadbalancing` service under Networking covering ELBv2 load balancers. First of the ungated AWS categories. Runtime contract notes: - Only ELBv2 is implemented. A Version=2012-06-01 request comes back in the 2015-12-01 namespace, so there is no separate classic ELB to model. - `State` is nested (<State><Code>active</Code></State>). Reading State directly yields an object and the column renders blank. - A missing load balancer is reported with HTTP 400, not 404. The wire <Code> is LoadBalancerNotFound but the SDK names the modelled error LoadBalancerNotFoundException — matching only the wire code passed every stubbed test and still returned 400 against the live runtime, so both names are accepted and both are in awsErrors.ts. - The runtime returns <NextMarker></NextMarker> at the end of a list, which the SDK surfaces as an empty string, so paging stops on falsy rather than undefined or it spins forever. A test pins that. - create needs at least two subnets in different availability zones. They are taken as comma separated ids, following awsComputeSchema which handles security group ids the same way, rather than pushing create into a bespoke panel. Target groups are deliberately excluded: they are a second resource kind in this category and would need the `kind` facet from ResourceQuery.filters (floci-io#162). Shipping load balancers alone keeps this independent of that PR. Adds 'load-balancer' to CloudResource.type in both packages — the frontend keeps its own union and there is no shared dependency, so a new resource kind is always a two-package change. Verified end to end through the route: nav entry under Networking, create with two real subnets, list showing the state reaching active, inspect by encoded ARN, 404 for a missing load balancer, all four validation rejections, and delete.
|
@hectorvent Thank you, and understood on the #145 coordination — I have deliberately not reworked the adapter or the taxonomy while that decision is open, so this push is limited to the two things you asked for that are independent of it. 1. The I chose that over building the control because rendering non-search filters is shared frontend work — every category with a facet needs it, and it would collide with whatever #145 lands. It belongs in its own PR rather than half-built here. If the maintainer decision goes toward this PR's shape and you would rather the select ship with it, I am happy to add it then. 2. Confirmed the README row is generated. Re-ran On the Gate green: lint, type-check, 459 tests, build. |
Adds a `loadbalancing` service under Networking covering ELBv2 load balancers. First of the ungated AWS categories. ## Runtime contract notes - **Only ELBv2 is implemented.** A `Version=2012-06-01` request comes back in the `2015-12-01` namespace, so there is no separate classic ELB to model. - **`State` is nested** (`<State><Code>active</Code></State>`). Reading `State` directly yields an object and the column renders blank. - **A missing load balancer is HTTP 400, not 404** — the fourth service where this runtime does that. Worth reading closely: the wire `<Code>` is `LoadBalancerNotFound`, but the SDK names the modelled error `LoadBalancerNotFoundException`. Matching only the wire code **passed every stubbed test and still returned 400 against the live runtime**, because my stub used the code I had seen in `curl` rather than the name the SDK produces. Both names are now accepted, both are in `awsErrors.ts`, and there is a test for each. - **The runtime returns `<NextMarker></NextMarker>`** at the end of a list, which the SDK surfaces as an empty string — paging stops on falsy rather than `undefined`, or it spins forever. A test pins that. - **`create` needs at least two subnets in different AZs.** They are taken as comma separated ids, following `awsComputeSchema`, which handles security group ids the same way — rather than pushing create into a bespoke panel as the AWS networking flows needed. ## Scope Target groups are deliberately excluded. They are a second resource kind in this category and would need the `kind` facet from `ResourceQuery.filters` (#162, still open). Shipping load balancers alone keeps this PR independent of that one; target groups are a clean follow-up once #162 lands. ## Verification `lint`, `type-check`, `test` and `build` pass from the repo root. 15 adapter tests. End to end through the route: nav entry under Networking, create with two real subnets from the default VPC, list showing the state reaching `active`, inspect by encoded ARN, **404** for a missing load balancer, all four validation rejections, and delete. ## Merge note Adds `'load-balancer'` to `CloudResource.type` in **both** packages. The frontend keeps its own union and there is no shared dependency, so a new resource kind is always a two-package change — #156 makes that permanently safe by opening the frontend type. Also adds four entries to the not-found `Set` in `awsErrors.ts`. --------- Co-authored-by: fredpena <f.ant.pena@gmail.com>
# [0.4.0](floci-io/floci-ui@0.3.0...0.4.0) (2026-09-01) ### Bug Fixes * **ec2:** include catalog AMIs in launch selector ([floci-io#191](floci-io#191)) ([b72135d](floci-io@b72135d)) ### Features * **aws:** add CloudFormation adapter to Cloud Explorer ([floci-io#184](floci-io#184)) ([f3d6105](floci-io@f3d6105)), closes [floci-io#81](floci-io#81) [floci-io#75](floci-io#75) [floci-io#81](floci-io#81) [floci-io#75](floci-io#75) [floci-io#81](floci-io#81) * **aws:** add EventBridge explorer ([floci-io#146](floci-io#146)) ([42944e9](floci-io@42944e9)), closes [floci-io#85](floci-io#85) * **aws:** add IAM to Cloud Explorer ([floci-io#145](floci-io#145)) ([fc50d19](floci-io@fc50d19)), closes [floci-io#79](floci-io#79) * **aws:** add Secrets Manager resource adapter ([floci-io#193](floci-io#193)) ([4811866](floci-io@4811866)) * **azure:** add databases and split Cosmos NoSQL ([floci-io#149](floci-io#149)) ([2e704f4](floci-io@2e704f4)), closes [floci-io#92](floci-io#92) [floci-io#67](floci-io#67) [floci-io/floci-az#138](floci-io/floci-az#138) [floci-io#143](floci-io#143) * **azure:** add Service Bus explorer ([floci-io#144](floci-io#144)) ([a9f0d06](floci-io@a9f0d06)), closes [floci-io#89](floci-io#89) * **eks:** Manage nodegroups and Fargate profiles via Cloud Proxy ([floci-io#194](floci-io#194)) ([3091cc9](floci-io@3091cc9)), closes [floci-io#106](floci-io#106) * **loadbalancing:** add an AWS Elastic Load Balancing adapter ([floci-io#168](floci-io#168)) ([10d4298](floci-io@10d4298)), closes [floci-io#162](floci-io#162) [floci-io#162](floci-io#162) [floci-io#156](floci-io#156) * **messaging:** add a messaging category with SQS and Pub/Sub ([floci-io#157](floci-io#157)) ([dfa6d1c](floci-io@dfa6d1c)), closes [floci-io#155](floci-io#155) [floci-io#155](floci-io#155) [floci-io#156](floci-io#156) [floci-io#156](floci-io#156) * **secretsmanager:** add JSON key-value editor for secret values ([floci-io#195](floci-io#195)) ([8e88961](floci-io@8e88961)), closes [floci-io#151](floci-io#151) * **ses:** Add AWS SES mailbox to Cloud Explorer ([floci-io#196](floci-io#196)) ([6389a56](floci-io@6389a56)), closes [floci-io#130](floci-io#130)
Adds an
iamservice under Security covering users, roles and customer-managedpolicies — and the small SPI change it needs.
Why the SPI change ships with a consumer
IAM holds three kinds of resource in one category, and
kind=rolesis not somethinga free-text search box can express. So this adds
ResourceQuery.filters, populatedby the list route.
I deliberately did not send the facet plumbing as its own PR.
sortableandcopyablewere removed fromTableColumnSchemain review for being unused, soschema surface with no consumer gets rejected here — rightly. This lands the facet
and its first real user together.
filtersis populated only for the facet names a service's own schema declares,so an undeclared or stale query param is ignored rather than reaching an adapter that
never asked for it — the same way the catalog gates unknown service slugs.
searchstays a separate field, so every existing adapter is untouched.
Decisions that follow from the API's shape
kind/identifier—user/alice,role/deployer, andpolicy/{arn}for policies, becauseGetPolicyandDeletePolicytake an ARNrather than a name. A bare name would be ambiguous across the three kinds and
would not tell
getordeletewhich API to call. The ids survive the genericroute because
HttpClient.ts:257encodes path params — verified with an encodedARN, not assumed.
Scope: 'Local'. Real IAM otherwise returns close toa thousand AWS-managed policies and buries the account's own. The emulator returns
few enough that only a test can hold this line.
which is unreadable raw in the inspector.
createvalidates per kind: a role needs a trust policy, a policy needs adocument, and both must parse as JSON. The message names the field and the kind,
because a flat form cannot mark a field conditionally required and the form shows
both for every kind.
Listcall pages onIsTruncated/Marker.Verification
lint,type-check,testandbuildpass from the repo root; both lockfilesinstall
--frozen-lockfileclean.25 adapter tests plus 4 route tests for the facet plumbing. Hermetic — the IAM,
route, capability-guard and catalog suites (149 tests) pass with
globalThis.fetchreplaced by a throw. I also confirmed the route tests genuinely fail without the
plumbing rather than passing by construction.
End to end through the route: nav entry, create for all three kinds, list with and
without the facet, an undeclared param being ignored, an unknown facet value
returning 400, inspect by encoded id including a policy ARN, the decoded trust
policy, delete for each kind, and 404 for a missing entity.
Merge note
Branched off
main. Adds'iam-user' | 'iam-role' | 'iam-policy'toCloudResource.type, which #156–#161 also widen with their own members, so the lastto merge resolves a one-line conflict. The
ResourceQueryand route changes areadditive and touched by no other open PR.
Follow-ups this unblocks: GCP IAM service accounts, and attach/detach of policies
once there is a generic resource actions route.